﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Security;
using System.Security.Cryptography;

namespace MoneyWentWhere.DataModel.Storage
{
    class EncryptedFormat : DataFormatX, IHasPassword
    {
        public String Password { get; set; } 

        const int MinPasswordLength = 4;
        public readonly DataFormatX InnerFormat;

        // NOTE: if I ever change this, I need to do more elaborate parsing 
        // matching within the stream when reading the file to strip off the header
        // (for backward compatibility)
        const String FileHeader = 
            "MoneyWentWhere Encrypted File\r\n" + 
            "http://code.google.com/p/moneywentwhere/\r\n" + 
            "\r\n";

        public EncryptedFormat(DataFormatX innerFormat)
        {
            InnerFormat = innerFormat;
        }

        public override bool AcceptsFile(string filename)
        {
            if (!File.Exists(filename)) { return false; }

            try
            {
                using (FileStream f = new FileStream(filename, FileMode.Open))
                {
                    // NOTE: we're not checking format beyond the header
                    return ContainsHeader(f);
                }
            }
            catch (Exception)
            {
                // TODO: log exception
                return false;
            }
        }

        bool ContainsHeader(Stream inStream)
        {
            byte[] bh = new byte[FileHeader.Length];
            inStream.Read(bh, 0, bh.Length);
            return ASCIIEncoding.ASCII.GetString(bh) == FileHeader;
        }

        public override void Save(MoneyDataSet ds, Stream outStream)
        {
            // TODO: throw if password is not set
            if (Password == null)
            {
                throw new ArgumentNullException("Password");
            }

            DESCryptoServiceProvider des = new DESCryptoServiceProvider();

            // Key must be 64-bit
            byte[] key = Create64BitKey(Password);
            des.Key = key;
            des.IV = key;

            byte[] bh = Encoding.ASCII.GetBytes(FileHeader);
            outStream.Write(bh, 0, bh.Length);

            using (CryptoStream cryptoStream = new CryptoStream(outStream,
                des.CreateEncryptor(), CryptoStreamMode.Write))
            {
                InnerFormat.Save(ds, cryptoStream);
            }
        }

        public override MoneyDataSet Load(Stream inStream)
        {
            DESCryptoServiceProvider des = new DESCryptoServiceProvider();

            byte[] key = Create64BitKey(Password);
            des.Key = key;
            des.IV = key;

            // Read the file header
            if (!ContainsHeader(inStream))
            {
                throw new FileFormatException("Encrypted file header not present.");
            }

            MoneyDataSet ds;
            try
            {
                using (CryptoStream cryptoStream = new CryptoStream(inStream,
                    des.CreateDecryptor(), CryptoStreamMode.Read))
                {
                        ds = InnerFormat.Load(cryptoStream);
                }
            }
            catch (CryptographicException e)
            {
                throw new InvalidPasswordException("Invalid password.", e);
            }

            return ds;
        }

        static byte[] Create64BitKey(String password)
        {
            SHA1 hashFn = SHA1.Create();
            byte[] hash = hashFn.ComputeHash(UnicodeEncoding.Unicode.GetBytes(password));

            const int KeyLength = 8;
            byte[] key = new byte[KeyLength];
            Array.Copy(hash, key, KeyLength);
            return key;
        }
    }
}
